/**
 * jQuery Plasma Plug-in, version 0.3
 * Copyright (c) 2011 Daniel MacDonald dpm@projectatomic.com ALL RIGHTS RESERVED
 * 
 * This is free software; you can redistribute it and/or modify 
 * it under the terms of the GNU Lesser General Public License 
 * as published by the Free Software Foundation; either version 
 * 2.1 of the License, or (at your option) any later version.
 * 
 * This software is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 * Lesser General Public License for more details.
 * 
 * 
 * This plug-in creates a table and cycles the background colors
 * of the table cell with an HSV Sine transformation. This plug-in
 * can become processor intensive with too many rows or columns,
 * which could cause a browser to react slowly, freeze, or crash.
 * The shift and blur options seem to have the most noticeable
 * effect on the plasma.
 * 
 * VERSION 0.3 (Beta 1)
 * added different plasma algorithms settings.type
 * added a limit to the time shift parameter
 * 
 * VERSION 0.2
 * optimized the delay timer
 * added start, stop, destroy methods
 * added data support for settings to allow on the fly changes
 * 
 * VERSION 0.1
 * initial release, not ready for prime time
 * 
 * ROADMAP:
 * fix bugs
 * create an overlay method
 * work on performance
 */

(function( $ ){

  var settings = {
    rows: 8, // more rows = more processing cycles
    cols: 8, // more cols = more processing cycles
    spacing: 0, // table's cellspacing attribute
    padding: 0, // table's cellpadding attribute
    border: 0, // table's border attribute
    height: '40px', // css height property for each cell
    width: '40px', // css height property for each cell
    html: '', // each cell can contain some html
    type: 0, // so far, 0 or 1 or 2, more to come
    shift: 0, // any positive integer
    limit: 1260, // any positive integer > shift how far to run the animation
    blur: 1, // less than 1 sharpens the plasma, greater than 1 blurs
    r: 1, // red intensity 0-1
    g: 1, // green intensity 0-1
    b: 1, // blue intensity 0-1
    h: 255, // hue multiplier
    s: 1, // saturation intensity 0-1
    v: 1, // value intensity 0-1
    delay: 100
  };
 
  var $this;
 
  var methods = {

/*
 * init method
 * creates table and sets interval timer for plasma
 */
    init: function(options) {
     
      return this.each(function(){
        $this = $(this);
        if (options) { 
          $.extend(settings, options);
        };
        var table = $('<table/>').attr({cellpadding:settings.padding, cellspacing:settings.spacing, border:settings.border});
        for (i = 0; i < settings.rows; i++) {
          var row = $('<tr/>');
          for (j = 0; j < settings.cols; j++) {
            row.append($('<td/>').css({height:settings.height, width:settings.width}).html(settings.html));
          };
          table.append(row);
        };
        $this.append(table);
        $this.data('plasma', settings);
        $.extend($this.data('plasma'), {shiftup: true});
        methods.plasma_morph();
      });
    },

/*
 * plasma_morph method (semi-private)
 * runs every delay option milliseconds
 * performs Sine wave transformation, cycles HSV colors, sets background color of each table cell
 */   
    plasma_morph: function() {
      var data = $this.data('plasma');
      for(y = 0; y < data.rows; y++) {
        for(x = 0; x < data.cols; x++) {
          var color = methods.set_color(x, y);
          methods.set_cell(x, y, color.r, color.g, color.b);
        };
      };
      
      if (data.shift < 0 || data.shift >= data.limit) {
        data.shiftup = !data.shiftup;
      };
      if (data.shiftup) {
        data.shift++;
      } else {
        data.shift--;
      };
      $.extend(data, {timeout: window.setTimeout(methods.plasma_morph, data.delay)})
    },
  
/*
 * set_color method (semi-private)
 * x integer column < settings.cols
 * y integer row < settings.row
 * returns color object {r, g, b} each integers 0-255
 */    
    set_color: function(x, y) {
      var data = $this.data('plasma');
      switch (data.type) {
        case 0: // blur sines and hsv conversion
          var value = Math.sin(methods.distance(x + data.shift, y, 128, 128) / data.cols / data.blur) 
            + Math.sin(methods.distance(x, y, 64, 64) / data.cols / data.blur) 
            + Math.sin(methods.distance(x, y  , 192, 64) / (data.rows - 1) / data.blur) 
            + Math.sin(methods.distance(x, y + data.shift/ (data.rows - 1), 192, 100) / data.rows / data.blur);
          var color = methods.HSVtoRGB(value * data.h / data.blur, data.s, data.v);
          break;
        case 1: // blur only hsv conversion
          var value = Math.sin(methods.distance(x + data.shift, y, 128, 128) / data.cols) 
            + Math.sin(methods.distance(x, y, 64, 64) / data.cols) 
            + Math.sin(methods.distance(x, y  , 192, 64) / (data.rows - 1)) 
            + Math.sin(methods.distance(x, y + data.shift / (data.rows - 1), 192, 100) / data.rows);
          var color = methods.HSVtoRGB(value * data.h / data.blur, data.s, data.v);
          break;
        case 2: // blur only sines
          var value = Math.sin(methods.distance(x + data.shift, y, 128, 128) / data.cols / data.blur) 
            + Math.sin(methods.distance(x, y, 64, 64) / data.cols / data.blur) 
            + Math.sin(methods.distance(x, y  , 192, 64) / (data.rows - 1) / data.blur) 
            + Math.sin(methods.distance(x, y + data.shift/ (data.rows - 1), 192, 100) / data.rows / data.blur);
          var color = methods.HSVtoRGB(value * data.h, data.s, data.v);
          break;
        default: // no adjustments
          var value = Math.sin(methods.distance(x + data.shift, y, 128, 128) / 8) 
            + Math.sin(methods.distance(x, y, 64, 64) / 8) 
            + Math.sin(methods.distance(x, y  , 192, 64) / 7) 
            + Math.sin(methods.distance(x, y + data.shift / 7, 192, 100) / 8);
          var color = methods.HSVtoRGB(value * data.h / data.blur, data.s, data.v);
      };
      color.r *= data.r;
      color.g *= data.g;
      color.b *= data.b;
      return color;
    },

/*
 * set_cell method (semi-private)
 * x integer column < settings.cols
 * y integer row < settings.row
 * r, g, b integer 0-255
 */    
    set_cell: function(x, y, r, g, b) {
      /** future development will include overlay functionality which will go here */
      $this.find('tr').eq(y).find('td').eq(x).css('background-color', 'rgb('+r+','+g+','+b+')');
    },

/*
 * HSVtoRGB method (semi-private)
 * h (hue) 0-360
 * s (saturation) 0-1
 * v (value) 0-1
 * returns object with r, g, b properties 0-255
 */    
    HSVtoRGB: function(h,s,v) { 
      var data = $this.data('plasma');
      var r,g,b,i,f,p,q,t;
      while (h < 0) { 
        h += 359; 
      };
      h %= 359;
      
      s = s > 1 ? 1 : s < 0 ? 0 : s;
      v = v > 1 ? 1 : v < 0 ? 0 : v;
      
      if (s == 0) {
        r = g = b = v;
      } else {
        h /= 60;
        f = h - (i = Math.floor(h));
        p = v * (1 - s);
        q = v * (1 - s * f);
        t = v * (1 - s * (1 - f));
        switch (i) {
          case 0: r = v; g = t; b = p; break;
          case 1: r = q; g = v; b = p; break;
          case 2: r = p; g = v; b = t; break;
          case 3: r = p; g = q; b = v; break;
          case 4: r = t; g = p; b = v; break;
          case 5: r = v; g = p; b = q; break;
        };
      };
      return {
        r: Math.floor(r*255),
        g: Math.floor(g*255),
        b: Math.floor(b*255)
      };
    },
 
/*
 * distance method (semi-private)
 * a, b, c, d numeric, not 0
 */    
    distance : function(a, b, c, d) {
      return Math.sqrt((c-a)*(c-a)+(d-b)*(d-b));
    },

/*
 * start method
 * restarts a stopped animation
 */    
    start : function() {
      return this.each(function(){
        if ($(this).data('plasma')) {
          methods.plasma_morph();
        };
      });
    },

/*
 * stop method
 * clears the timeout to plasma_morph method
 */    
    stop : function() {
      return this.each(function(){
        if ($(this).data('plasma')) {
          window.clearTimeout($(this).data('plasma').timeout);
        };
      });
    },

/*
 * destroy method
 * clears the timeout to plasma_morph method
 * removes stored data
 */    
    destroy : function() {
      return this.each(function(){
        if ($(this).data('plasma')) {
          window.clearTimeout($(this).data('plasma').timeout);
          $(this).removeData('plasma').empty();
        };
      });
    }
  };

/*
 * jQuery plug-in declaration
 */
  $.fn.plasma = function(method) {
    if (methods[method]) {
      return methods[method].apply(this, Array.prototype.slice.call(arguments, 1));
    } else if (typeof method === 'object' || !method) {
      return methods.init.apply(this, arguments);
    } else {
      $.error('Method ' + method + ' does not exist on jQuery.plasma');
    };   
  };

})(jQuery);
